/**
 *  @file X509Certificate.cpp
 */
#include "X509Certificate.h"
#include "openssl/x509v3.h" 
#include "openssl/bio.h" 
#include "openssl/ssl.h" 
#include "openssl/pem.h" 
#include "../../io/FileStream.h"
#include "../../io/IOException.h"
#include "../__internal/InitOpenSSL.h"
#include "../../exceptions/ArgumentException.h"
#include "../../common/CppflibMain.h"
#include "../../common/ObjPtrT.h"
#include <stdio.h>

using namespace cppflib;
using namespace cppflib::util;
using namespace cppflib::collections;
using namespace cppflib::io;
using namespace cppflib::exceptions;

namespace cppflib
{

namespace security
{

namespace cert
{

/**
 *  Convert ASN1_TIME to ticks suitable for a DateTime object
 */
PRIVATE i64_t __ASN1Time2DateTimeTicks(ASN1_TIME * pAsn1Time)
{
   // ref from: a_gentm.c and a_utctm.c
   const char * format = (pAsn1Time->type == V_ASN1_GENERALIZEDTIME) ? "%04d%02d%02d%02d%02d%02dZ" :
                                       (pAsn1Time->type == V_ASN1_UTCTIME) ? "%02d%02d%02d%02d%02d%02dZ" : NULL;
   
   if (format != NULL) {
      int tempTime[6];
      sscanf(reinterpret_cast<char*>(pAsn1Time->data), format, &tempTime[0], &tempTime[1], &tempTime[2],
                                                               &tempTime[3], &tempTime[4], &tempTime[5]);

      // V_ASN1_UTCTIME represent date between 1950 and 2050
      if (pAsn1Time->type == V_ASN1_UTCTIME) {
         if (tempTime[0] < 50)
            tempTime[0] += 100;
         tempTime[0] += 1900;
      }

      DateTime d(tempTime[0], tempTime[1], tempTime[2], tempTime[3], tempTime[4], tempTime[5], DateTime::DTK_UTC);
      return d.Ticks();
   }

   return 0;
}


PRIVATE X509 * __GetX509(ptrany_t x509)
{
   return static_cast<X509*>(x509);
}

X509Certificate::X509Certificate(void)
{
   cppflib::security::__internal::InitOpenSSL();

   this->x509Cert = NULL;
   this->isFreeCert = true;
}

/**
 *  @param [in] x509Cert -- a pointer to the OpenSSL X509 cert
 *  @param [in] isFreeCert -- whether to free the cert in destructor
 */
X509Certificate::X509Certificate(ptrany_t x509Cert, bool isFreeCert)
{
   cppflib::security::__internal::InitOpenSSL();

   this->x509Cert = x509Cert;
   this->isFreeCert = isFreeCert;

   if (!x509Cert)
      THROW_CTOR_EXCEP(ArgumentException, _S("X509 cert is NULL"));   
}

X509Certificate::~X509Certificate(void)
{
   ReleaseBeforeThrowing();
}

void X509Certificate::ReleaseBeforeThrowing()
{
   if (x509Cert) {
      if (isFreeCert) X509_free(__GetX509(x509Cert));
      x509Cert = NULL;
   }

   cppflib::security::__internal::UninitOpenSSL();
}

/**
 *  Get X509 cert version
 *
 *  @return 0 -- version 1
 *  @return 1 -- version 2
 *  @return 2 -- version 3
 */
int X509Certificate::GetVersion()
{
   return X509_get_version(__GetX509(x509Cert));
}

/**
 *  @param [in] x509Name -- input name, 'struct X509_name_st' is the same as 'X509_NAME'
 *  @param [out] s -- output string
 */
PRIVATE void __GetStringFromX509Name(struct X509_name_st *pName, CFString &s)
{
   BIO *bioMem = BIO_new(BIO_s_mem());
   X509_NAME_print_ex(bioMem, pName, 0, XN_FLAG_RFC2253);
   BIO_write(bioMem, "\0", 1);  // ensure null-terminated

   char *dataPtr = NULL;
   BIO_get_mem_data(bioMem, &dataPtr);
   if (dataPtr)
      s = static_cast<const char*>(dataPtr);
   
   BIO_free(bioMem);
}

/**
 *  Get issuer distinguished name
 */
CFString X509Certificate::GetIssuerName()
{
   CFString s("");
   __GetStringFromX509Name(X509_get_issuer_name(__GetX509(x509Cert)), s);

   return s;
}

/**
 *  @param [in] x509Name -- input name, 'struct X509_name_st' is the same as 'X509_NAME'
 */
PRIVATE bool __GetOneValueFromX509Name(struct X509_name_st * pName, const CFString &longOrShortName, CFString &outVal)
{
   const int NAMEBUFSIZE = 65; // should be big enough
   char nameBuf[NAMEBUFSIZE];
   longOrShortName.ToUtf8String(nameBuf, NAMEBUFSIZE, NULL);

   int nid = OBJ_sn2nid(nameBuf);  // try short name
   if (nid == NID_undef)
      nid = OBJ_ln2nid(nameBuf);   // try long name

   if (nid == NID_undef)
      return false;
   
   // get length first
   int len = X509_NAME_get_text_by_NID(pName, nid, NULL, 0); 
   collections::ArrayByte valBuf(len + 1);

   // actual conversion
   X509_NAME_get_text_by_NID(pName, nid, reinterpret_cast<char*>(valBuf.GetRawPtr()), static_cast<int>(valBuf.GetSize())); 
   outVal = reinterpret_cast<const char*>(valBuf.GetRawPtr());

   return true;
}

/**
 *  Issuer name looks like
 *
 *  OU=SSS CA Inc,O=SSS CA Ltd,emailAddress=hello@sss.com,C=HK,ST=China,CN=SSS CA Inc
 *
 *  This method retrieve one of the value in the issuer
 *
 *  @param [in] longOrShortName -- can be long name (e.g. "commonName") or short name (e.g. "CN")
 *  @param [in, out] outVal -- output value
 *
 *  @return true -- OK
 *  @return false -- failed
 */
bool X509Certificate::GetOneValueFromIssuerName(const CFString &longOrShortName, CFString &outVal)
{
   return __GetOneValueFromX509Name(X509_get_issuer_name(__GetX509(x509Cert)), longOrShortName, outVal);
}

/**
 *  Get subject distinguished name
 */
CFString X509Certificate::GetSubjectName()
{
   CFString s("");
   __GetStringFromX509Name(X509_get_subject_name(__GetX509(x509Cert)), s);

   return s;
}

/**
 *  Subject name looks like
 *
 *  OU=Server Unit,O=Server Ltd,emailAddress=hello@myserver.com,C=HK,ST=China,CN=*.myserver.com
 *
 */
bool X509Certificate::GetOneValueFromSubjectName(const CFString &longOrShortName, CFString &outVal)
{
   return __GetOneValueFromX509Name(X509_get_subject_name(__GetX509(x509Cert)), longOrShortName, outVal);
}

/**
 *  Returns 'notBefore' date from validity period
 */
DateTime X509Certificate::GetNotBefore()
{
   ASN1_TIME *pTime = X509_get_notBefore(__GetX509(x509Cert));
   DateTime d(__ASN1Time2DateTimeTicks(pTime));
   return d;
}

/**
 *  Returns 'notAfter' date from validity period
 */
DateTime X509Certificate::GetNotAfter()
{
   ASN1_TIME *pTime = X509_get_notAfter(__GetX509(x509Cert));
   DateTime d(__ASN1Time2DateTimeTicks(pTime));
   return d;
}

/**
 *  Check whether cert is valid at the specified date
 */
bool X509Certificate::IsValid(util::DateTime &d)
{
   DateTime before = GetNotBefore();
   DateTime after = GetNotAfter();

   return (d >= before && d <= after);
}

#define	X509_get_encoded(x) (&((x)->cert_info->enc))

/**
 *  Return the encoded form of the cert
 */
ArrayByte *X509Certificate::GetEncoded()
{
   ASN1_ENCODING *pEncoded = X509_get_encoded(__GetX509(x509Cert));
   if (pEncoded->modified == 0 && pEncoded->len > 0) {
      ArrayByte *pBuf = new ArrayByte(pEncoded->len);
      pBuf->CopyFrom(pEncoded->enc, pEncoded->len, 0, 0, pEncoded->len);
      return pBuf;
   }

   return NULL;
}

/**
 *  Get number of extensions in this cert
 */
int X509Certificate::GetExtensionCount()
{
   return X509_get_ext_count(__GetX509(x509Cert));
}

/**
 *  Get extension name
 * 
 *  @param [in] index -- 0 to GetExtensionCount() - 1
 *  @param [out] extName -- to retrieve the extension name
 */
bool X509Certificate::GetExtensionName(int index, CFString &extName)
{
   X509_EXTENSION *ext = X509_get_ext(__GetX509(x509Cert), index); 
   if (!ext)
      return false;

   const char * extStr = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(ext)));
   if (extStr) {
      extName = extStr;
      return true;
   }

   return false;
}

/**
 *  Get extension type
 *
 *  @param [out] type -- a list, string or raw binary
 */
bool X509Certificate::GetExtensionType(int index, X509Certificate::extensiontype_t &type)
{
   type = X509Certificate::EXT_UNKNOWN;

   X509_EXTENSION *ext = X509_get_ext(__GetX509(x509Cert), index); 
   if (!ext)
      return false;

   const X509V3_EXT_METHOD *method = X509V3_EXT_get(ext);
   if (!method)
      return false;

   if (method->i2v) type = X509Certificate::EXT_LIST;
   else if (method->i2s) type = X509Certificate::EXT_STRING;
   else if (method->i2r) type = X509Certificate::EXT_RAW;
   
   return (type != X509Certificate::EXT_UNKNOWN);
}

/**
 *  Free internal structure retrieved by X509V3_EXT_d2i
 */
PRIVATE void __FreeExtensionInternalStructure(void * pExtStruct, const X509V3_EXT_METHOD *method)
{
   if (method->it)
      ASN1_item_free(reinterpret_cast<ASN1_VALUE*>(pExtStruct), ASN1_ITEM_ptr(method->it));
   else if (method->ext_free)
      method->ext_free(pExtStruct);
   else 
      OPENSSL_free(pExtStruct);  // TODO: I am not sure what to do here
}

/**
 *  Get extension list
 *
 *  @param [out] extValList -- a list of name-value pairs. Can't use LinkedHashtable here as the list 
 *                             may have more than one key with the same name
 */
bool X509Certificate::GetExtensionValueList(int index, NameValueList &extValList)
{

   X509_EXTENSION *ext = X509_get_ext(__GetX509(x509Cert), index); 
   if (!ext)
      return false;

   const X509V3_EXT_METHOD *method = X509V3_EXT_get(ext);
   if (!method || !method->i2v) 
      return false; 

   void * pExtStruct = NULL;
   STACK_OF(CONF_VALUE) *pVal = NULL;
   bool isOK = false;
   do {
      pExtStruct = X509V3_EXT_d2i(ext);  // get internal structure
      if (!pExtStruct)
         break;

      pVal = method->i2v(method, pExtStruct, NULL); // convert to readable value
      if (!pVal)
         break;
      
      // convert to our structure 
      for (int i = 0; i < sk_CONF_VALUE_num(pVal); i++) { 
         CONF_VALUE * nval = sk_CONF_VALUE_value(pVal, i);
         extValList.AddTail(new CFString(nval->name), new CFString(nval->value));
      }

      isOK = true;
   } while(false);

   if (pExtStruct)
      __FreeExtensionInternalStructure(pExtStruct, method);

   if (pVal)
      sk_CONF_VALUE_pop_free(pVal, X509V3_conf_free);

   return isOK;
}

/**
 *  Get extension string
 */
bool X509Certificate::GetExtensionValueString(int index, CFString &extValString)
{
   X509_EXTENSION *ext = X509_get_ext(__GetX509(x509Cert), index); 
   if (!ext)
      return false;

   const X509V3_EXT_METHOD *method = X509V3_EXT_get(ext);
   if (!method || !method->i2s) 
      return false; 

   void * pExtStruct = NULL;
   char * pVal = NULL;
   bool isOK = false;
   do {
      pExtStruct = X509V3_EXT_d2i(ext);  // get internal structure
      if (!pExtStruct)
         break;

      pVal = method->i2s(method, pExtStruct); // convert to readable value
      if (!pVal)
         break;
      
      // convert to our structure 
      extValString = pVal;
      isOK = true;
   } while(false);

   if (pExtStruct)
      __FreeExtensionInternalStructure(pExtStruct, method);

   if (pVal)
      OPENSSL_free(pVal);

   return isOK;
}

/**
 *  Get extension raw binary
 */
bool X509Certificate::GetExtensionValueRaw(int index, BinBuf &extValBin)
{

   X509_EXTENSION *ext = X509_get_ext(__GetX509(x509Cert), index); 
   if (!ext)
      return false;

   const X509V3_EXT_METHOD *method = X509V3_EXT_get(ext);
   if (!method || !method->i2r) 
      return false; 

   void * pExtStruct = NULL;
   BIO *bioMem = NULL;
   bool isOK = false;

   do {
      pExtStruct = X509V3_EXT_d2i(ext);  // get internal structure
      if (!pExtStruct)
         break;

      bioMem = BIO_new(BIO_s_mem());
      if (!method->i2r(method, pExtStruct, bioMem, 0)) // write to a bio
         break;

      u8_t *dataPtr = NULL;
      int nrByte = BIO_get_mem_data(bioMem, &dataPtr);
      if (nrByte <= 0 || !dataPtr)
         break;

      extValBin.CopyData(dataPtr, nrByte);  // copy to our buffer
      isOK = true;

   } while(false);

   if (pExtStruct)
      __FreeExtensionInternalStructure(pExtStruct, method);

   if (bioMem)
      BIO_free(bioMem);

   return isOK;
}

PRIVATE ArrayByte * __ReadBytesFromFile(pcwstr_t filePath)
{
   FileStream fileStream(filePath, FileStream::FA_READONLY);
   u32_t fileSize = static_cast<u32_t>(fileStream.GetFileSize());
   ArrayByte *readBuf = new ArrayByte(fileSize);
   if (fileStream.Read(*readBuf) < static_cast<int>(fileSize)) {
      cppmempool::Release(readBuf);
      throw IOException(_S("Reading bytes in a X509 cert error !"));
   }

   return readBuf;
}

/**
 *  Read X509 from a DER format file
 */
X509Certificate * X509Certificate::FromDER(pcwstr_t DERPath)
{
   ObjPtrT<ArrayByte> readBuf(__ReadBytesFromFile(DERPath));
   const u8_t * pCertBytes = readBuf->GetRawPtr();
   X509 * pCert = d2i_X509(NULL, &pCertBytes, static_cast<int>(readBuf->GetSize()));

   if (!pCert) {
      throw IOException(_S("Decode X509 DER cert error !"));
   }
   
   return new X509Certificate(static_cast<ptrany_t>(pCert), true);
}

/**
 *  Read X509 from a PEM format file
 */
X509Certificate * X509Certificate::FromPEM(pcwstr_t PEMPath)
{
   ObjPtrT<ArrayByte> readBuf(__ReadBytesFromFile(PEMPath));
   // create a read only memory BIO as 'PEM_read' requires a BIO object
   BIO * bioMem = BIO_new_mem_buf(readBuf->GetRawPtr(), static_cast<int>(readBuf->GetSize()));
   X509 * pCert = PEM_read_bio_X509(bioMem, NULL, NULL, NULL);

   BIO_free(bioMem);

   if (!pCert) {
      throw IOException(_S("Decode X509 PEM cert error !"));
   }
   
   return new X509Certificate(static_cast<ptrany_t>(pCert), true);
}


} // end of namespace cert

} // end of namespace security

} // end of namespace cppflib
